晓晓的个人博客Logo
晓晓的个人博客
Defer 与 Web Component 协同应用的最佳抓取时机
AI提炼icon
提炼
文章聚焦于在 “服务端生成的 HTML 页面 + JS 以 defer 加载”,特别是 “SSR/SSG + Web Component + defer 加载 script 脚本” 场景下,探讨页面初始数据的最佳抓取时机。现代 Web 应用常采用此类技术组合,以兼顾 SEO 和用户交互体验。defer 不仅不阻塞 HTML 解析,还在 DOM 树构建完成后、DOMContentLoaded 执行前执行,Web Component 从定义到可用历经解析、注册升级两阶段。解析阶段创建 DOM 实例并设置属性,但此时无行为和样式,也无法获取私有属性初始值;注册升级阶段,defer 脚本执行 customElements.define,浏览器遍历 DOM 树升级组件,触发相关回调,使组件功能完整,此时可获取所有属性初始值。综上,DOMContentLoaded 事件触发后是抓取初始数据的最佳时机,此时标准 DOM 元素和 Web Component 实例创建且私有属性初始化完毕,可安全访问。
本文于 2025-09-01 23:27 首次发布,最后修改于 2025-09-01 23:32

直击答案

当源网站应用建立在“SSR/SSG + Web Component + defer 加载 script 脚本”的技术基础上时,它的初始数据的最佳抓取时机是 在 DOMContentLoaded 事件触发之后。

答疑解惑

当我们试图用爬虫抓取现代 Web 应用的数据时,常常会遇到一些自定义标签,比 <user-profile>、<product-card> 等,再结合当下前端流行的几种打法,我们很容易就判断出源网站采用的应该是 SSR/SSG + Web Component 这套组合拳。

再者,为了同时兼具 SEO 和 用户侧极致的交互体验,源网站的工程师们会采取最佳实践方案:defer + Web Component。因为这套方案建立了一种精妙的时序控制策略,为页面数据的及时呈现奠定了技术基础。(P.S. 有些网站使用 type="module" 来加载 script 脚本,原理也是一样的,因为它自带 defer 的行为能力)

接下来,本文将带你深入探索这一渲染协同机制的核心原理。通过理解 defer 与 Web Component 的工作时序,精准把握此类网站的初始数据的最佳抓取时机。

首先,很多人认为 defer 只是“一种不阻塞解析HTML的脚本加载方式”,但事实上,这只是它的一小点优势,它的真正威力在于其严格的执行时机:DOM 树完全构建完成之后、DOMContentLoaded 执行之前(或者应该说,是所有的 defer 脚本执行完毕后才触发 DOMContentLoaded 事件);

另外,Web Component 本身从定义到可用会经历“解析、注册升级”两个阶段,再基于 defer 脚本确定的执行时机,Web Component 元素的可交互状态得以被明确定义。这使得我们能够精准预测元素完成初始化、可被安全调用的确切时间点:

1. Web Component 的解析阶段

  1. 这个阶段发生在:从服务器端获取到 HTML 文件后,进行 HTML 解析时;

  2. 在生成 DOM 实例时(当遇到 <user-profile> 这样的标签时):

    1. 浏览器会严格按照解析普通 HTML 的规则来处理它;

    2. 会立即创建这个元素的 DOM 实例,并将其添加到 DOM 树中相应的位置;

    3. 同时,也会立即设置它的标准属性和自定义的 attribute 属性(如 title="xxx"、date-title="xxx");

  3. 当解析阶段完成后,元素的状态变为:

    1. 此时,这个 <user-profile> 的 DOM 实例是一个 HTMLUnknownElement;

    2. 它没有任何行为能力或样式,因为行为是靠 JS 类定义的,而没有样式是因为浏览器不会给 Web Component 元素设置任何默认样式(后续,样式的最佳实践应该是由 CSS 作用于它);

    3. 关键的一点是:虽然属性已经被设置在了 DOM 对象上,但因为对应的 Web Component 元素定义尚不存在,所以不会触发任何 Web Component 的生命周期回调(如 attributeChangedCallback);

    4. 此时,你可以从该节点上获取 HTML 的标准属性的初始值,比如 id 等,也可以通过 getAttribute 获取属性的初始值,但是无法获取它的私有属性的初始值,因为它的私有属性是靠 JS 的 customElements.define 定义的;

  4. 这个阶段就好比房子的主体结构和所有房间( DOM 节点)都已经盖好,家具(属性/文本内容)也已搬入指定位置,但还没有通电通水;

2.Web Component 的注册与升级阶段

  1. 这个阶段发生在:DOM 树构建完成,defer 脚本下载完成并开始执行 customElements.define 时;

  2. 当执行到 customElements.define('user-profile', UserProfile)  这一行时,浏览器会进行一个称为 “升级” 的关键操作(“水合”数据):

    1. 浏览器会遍历整个 DOM 树,查找所有名为 user-profile 的标签;

    2. 对于每一个已经存在的 <user-profile>实例,浏览器会:

      1. 将其内部的原型(__proto__)从 HTMLUnknownElement 替换为 UserProfile;

      2. 调用其 constructor() 来初始化组件(创建 Shadow DOM、挂载私有属性 等);

      3. 因为它在解析时已经具有了 title 等属性,浏览器会自动触发一次 attributeChangedCallback。这样,你的组件就能用服务端设置的属性值进行首次渲染了;

      4. 调用 connectedCallback() ,调用完成后说明元素已经被定义且可用;

  3. 当解析阶段完成后,元素的状态变为:

    1. 元素现在是一个功能完整的 Web Component;

    2. 此后属性变化都会正常触发 attributeChangedCallback;

    3. 它们的 Shadow DOM 已经创建,内部样式已生效,初始属性已被处理;

    4. 此时 你可以从该节点上获取 HTML 的标准属性、也可以通过 getAttribute 获取属性、还可以获取它的私有属性的初始值;

  4. 也就是此时,电工和水管工( defer 脚本) 进场,按照蓝图(脚本顺序)给每个房间接通水电、安装灯具(为组件注入行为,“升级”组件),等这个阶段完成之后,工头宣布:“房子现在完全准备好了,可以入住了!” 这意味着所有的功能都已就绪;

Web Component 与 defer 脚本的解析时间线
Web Component 与 defer 脚本的解析时间线

综上,defer 的执行时机直接决定了 Web Component 元素的可用时机。这消除了其生命周期中的不确定性,为开发者和爬虫程序提供了一个明确的等待目标。即,针对“服务端生成的HTML页面 + JS 在 <head> 中以 defer 加载”的场景,我们就掌握了一个十分确定的抓取时机:在 DOMContentLoaded 事件触发后可以立马获取到 DOM 节点的初始数据,包括标准属性和私有属性。因为,在 DOMContentLoaded 事件触发后,标准 DOM 元素 和 Web Component 的实例不仅已经创建,而且其所有私有属性也已经初始化完毕,可以安全访问。

1个赞
喜欢就点个赞吧